Skip to content

S01-20 JavaSE-项目:坦克大战

[TOC]

坦克大战游戏演示

游戏演示

(文档中提及游戏演示,无具体截图,保留原说明)

为什么写这个项目

  1. 涵盖 Java 多方面技术:面向对象、多线程、文件 I/O、数据库
  2. 巩固旧知识,学习新知识
  3. 趣味性强

写项目前的提醒

  1. 需具备一定 Java 基础,核心部分将逐步讲解
  2. 编程高手秘诀:思考 → 编程 → 思考 → 编程

授课原则

  1. 通俗易懂
  2. 不遗漏细节
  3. 版本迭代式开发(从 1.0 到最终版)

Java 绘图坐标体系

坐标体系-介绍

  • 坐标原点位于左上角,以像素为单位
  • 第一个参数为 x 坐标(水平方向,距离原点像素数)
  • 第二个参数为 y 坐标(垂直方向,距离原点像素数)

坐标体系-像素

  1. 像素是屏幕显示的基本单位
  2. 分辨率示例:800x600 表示屏幕每行 800 个像素,共 600 行(总计 480,000 个像素)
  3. 像素是密度单位,厘米是长度单位,不可直接比较

绘图快速入门(DrawCircle.java)

java
package com.hspedu.draw;

import javax.swing.*;
import java.awt.*;

/**
 * 演示如何在面板上画出圆形
 * @author 韩顺平
 * @version 1.0
 */
@SuppressWarnings({"all"})
public class DrawCircle extends JFrame {
    // 定义面板
    private MyPanel mp = null;

    public static void main(String[] args) {
        new DrawCircle();
        System.out.println("退出程序~");
    }

    public DrawCircle() { // 构造器
        // 初始化面板
        mp = new MyPanel();
        // 把面板放入窗口
        this.add(mp);
        // 设置窗口大小
        this.setSize(400, 300);
        // 点击窗口关闭按钮,程序完全退出
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // 显示窗口
        this.setVisible(true);
    }

    // 自定义面板类,继承 JPanel,用于绘图
    class MyPanel extends JPanel {
        /**
         * 绘图方法
         * @param g 画笔对象
         */
        @Override
        public void paint(Graphics g) {
            super.paint(g); // 调用父类方法完成初始化
            System.out.println("paint 方法被调用了~");

            // 1. 画直线 drawLine(int x1, int y1, int x2, int y2)
            // g.drawLine(10, 10, 100, 100);

            // 2. 画矩形边框 drawRect(int x, int y, int width, int height)
            // g.drawRect(10, 10, 100, 100);

            // 3. 填充矩形 fillRect(int x, int y, int width, int height)
            g.setColor(Color.blue);
            // g.fillRect(10, 10, 100, 100);

            // 4. 画椭圆边框 drawOval(int x, int y, int width, int height)
            // g.drawOval(10, 10, 100, 100);

            // 5. 填充椭圆 fillOval(int x, int y, int width, int height)
            g.setColor(Color.red);
            // g.fillOval(10, 10, 100, 100);

            // 6. 画图片 drawImage(Image img, int x, int y, ...)
            // Image image = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/bg.png"));
            // g.drawImage(image, 10, 10, 175, 221, this);

            // 7. 画字符串 drawString(String str, int x, int y)
            g.setColor(Color.red);
            g.setFont(new Font("隶书", Font.BOLD, 50));
            g.drawString("北京你好", 100, 100);
        }
    }
}

绘图原理

  1. Component 类提供核心绘图方法:
    • paint(Graphics g): 绘制组件外观
    • repaint(): 刷新组件外观
  2. paint() 自动调用场景:
    • 组件第一次显示时
    • 窗口最小化后最大化
    • 窗口大小改变时
    • 调用 repaint() 方法时

Graphics 类(画笔)

核心方法列表:

方法功能描述
drawLine(int x1, int y1, int x2, int y2)画直线
drawRect(int x, int y, int width, int height)画矩形边框
drawOval(int x, int y, int width, int height)画椭圆边框
fillRect(int x, int y, int width, int height)填充矩形
fillOval(int x, int y, int width, int height)填充椭圆
drawImage(Image img, int x, int y, ...)画图片
drawString(String str, int x, int y)画字符串
setFont(Font font)设置字体
setColor(Color c)设置颜色

绘出坦克(坦克大战 1.0 版)

核心代码(HspTankGame01.java)
java
package com.hspedu.tankgame;

import javax.swing.*;
import java.awt.*;

/**
 * 坦克大战游戏主窗口
 * @author 韩顺平
 * @version 1.0
 */
public class HspTankGame01 extends JFrame {
    // 定义绘图面板
    MyPanel mp = null;

    public static void main(String[] args) {
        new HspTankGame01();
    }

    public HspTankGame01() {
        mp = new MyPanel();
        this.add(mp); // 加入面板
        this.setSize(1000, 750); // 窗口大小
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 关闭窗口退出程序
        this.setVisible(true); // 显示窗口
    }
}

/**
 * 坦克大战绘图区域
 * @author 韩顺平
 * @version 1.0
 */
class MyPanel extends JPanel {
    // 定义我方坦克
    Hero hero = null;

    public MyPanel() {
        hero = new Hero(100, 100); // 初始化坦克位置
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillRect(0, 0, 1000, 750); // 填充背景(黑色)
        // 绘制坦克
        drawTank(hero.getX(), hero.getY(), g, 0, 0);
    }

    /**
     * 绘制坦克方法
     * @param x 坦克左上角 x 坐标
     * @param y 坦克左上角 y 坐标
     * @param g 画笔
     * @param direct 坦克方向(0-上,1-右,2-下,3-左)
     * @param type 坦克类型(0-我方,1-敌人)
     */
    public void drawTank(int x, int y, Graphics g, int direct, int type) {
        // 设置坦克颜色
        switch (type) {
            case 0: // 我方坦克(青色)
                g.setColor(Color.cyan);
                break;
            case 1: // 敌人坦克(黄色)
                g.setColor(Color.yellow);
                break;
        }

        // 根据方向绘制坦克
        switch (direct) {
            case 0: // 向上
                g.fill3DRect(x, y, 10, 60, false); // 左轮子
                g.fill3DRect(x + 30, y, 10, 60, false); // 右轮子
                g.fill3DRect(x + 10, y + 10, 20, 40, false); // 坦克身体
                g.fillOval(x + 10, y + 20, 20, 20); // 坦克盖子
                g.drawLine(x + 20, y + 30, x + 20, y); // 炮筒
                break;
            // 其他方向(右、下、左)省略,完整代码见后续版本
            default:
                System.out.println("暂时没有处理");
        }
    }
}

/**
 * 坦克父类
 * @author 韩顺平
 * @version 1.0
 */
class Tank {
    private int x; // 横坐标
    private int y; // 纵坐标

    public Tank(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // getter 和 setter 方法
    public int getX() { return x; }
    public void setX(int x) { this.x = x; }
    public int getY() { return y; }
    public void setY(int y) { this.y = y; }
}

/**
 * 我方坦克类
 * @author 韩顺平
 * @version 1.0
 */
class Hero extends Tank {
    public Hero(int x, int y) {
        super(x, y);
    }
}

绘图练习

运用 Java 绘图技术绘制以下图形:

  1. 蛤蟆
  2. 王八
  3. 小老鼠

Java 事件处理机制

案例:键盘控制小球移动(BallMove.java)

java
package com.hspedu.event_;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

/**
 * 演示键盘控制小球移动
 * @author 韩顺平
 * @version 1.0
 */
public class BallMove extends JFrame {
    MyPanel mp = null;

    public static void main(String[] args) {
        new BallMove();
    }

    public BallMove() {
        mp = new MyPanel();
        this.add(mp);
        this.setSize(400, 300);
        // 窗口监听键盘事件
        this.addKeyListener(mp);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }

    // 面板类,实现 KeyListener 接口
    class MyPanel extends JPanel implements KeyListener {
        int x = 10; // 小球左上角 x 坐标
        int y = 10; // 小球左上角 y 坐标

        @Override
        public void paint(Graphics g) {
            super.paint(g);
            g.fillOval(x, y, 20, 20); // 绘制小球
        }

        // 有字符输入时触发(未使用)
        @Override
        public void keyTyped(KeyEvent e) {}

        // 按键按下时触发
        @Override
        public void keyPressed(KeyEvent e) {
            // 根据按键控制小球移动
            if (e.getKeyCode() == KeyEvent.VK_DOWN) { // 下箭头
                y++;
            } else if (e.getKeyCode() == KeyEvent.VK_UP) { // 上箭头
                y--;
            } else if (e.getKeyCode() == KeyEvent.VK_LEFT) { // 左箭头
                x--;
            } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) { // 右箭头
                x++;
            }
            this.repaint(); // 重绘面板
        }

        // 按键松开时触发(未使用)
        @Override
        public void keyReleased(KeyEvent e) {}
    }
}

基本说明

  • Java 事件处理采用委派事件模型
  • 事件发生时,事件源将事件信息传递给事件监听者处理
  • 事件对象保存事件相关信息(如 KeyEvent 包含按键编码)

事件处理机制示意图

用户操作 → 事件源 → 事件对象 → 事件监听者 → 事件处理方法

核心概念

  1. 事件源:产生事件的对象(如按钮、窗口、键盘)
  2. 事件:承载事件源状态改变的对象,常见类型如下:
事件类型说明
ActionEvent按下按钮、双击列表项、选中菜单时发生
AdjustmentEvent操作滚动条时发生
ComponentEvent组件隐藏、移动、改变大小时发生
ContainerEvent组件加入/删除容器时发生
FocusEvent组件获得/失去焦点时发生
ItemEvent复选框、列表项被选中时发生
KeyEvent键盘按键按下/松开时发生
MouseEvent鼠标拖动、移动、点击时发生
TextEvent文本区/文本框内容改变时发生
WindowEvent窗口激活、关闭、最小化时发生
  1. 事件监听器:实现事件监听器接口的类,用于处理事件
    • 一个类可实现多个监听器接口
    • 接口位于 java.awt.eventjavax.swing.event

坦克大战游戏(2.0 版)

功能:按键控制坦克移动(W/D/S/A)

核心代码(HspTankGame02.java)
java
package com.hspedu.tankgame2;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;

/**
 * 坦克大战 2.0 版(支持坦克移动和敌人坦克绘制)
 * @author 韩顺平
 * @version 1.0
 */
public class HspTankGame02 extends JFrame {
    MyPanel mp = null;

    public static void main(String[] args) {
        new HspTankGame02();
    }

    public HspTankGame02() {
        mp = new MyPanel();
        this.add(mp);
        this.setSize(1000, 750);
        this.addKeyListener(mp); // 监听键盘事件
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }
}

/**
 * 敌人坦克类
 * @author 韩顺平
 * @version 1.0
 */
class EnemyTank extends Tank {
    public EnemyTank(int x, int y) {
        super(x, y);
    }
}

/**
 * 我方坦克类
 * @author 韩顺平
 * @version 1.0
 */
class Hero extends Tank {
    public Hero(int x, int y) {
        super(x, y);
    }
}

/**
 * 绘图面板(实现键盘监听)
 * @author 韩顺平
 * @version 1.0
 */
class MyPanel extends JPanel implements KeyListener {
    Hero hero = null;
    // 敌人坦克集合(Vector 线程安全)
    Vector<EnemyTank> enemyTanks = new Vector<>();
    int enemyTankSize = 3; // 敌人坦克数量

    public MyPanel() {
        hero = new Hero(100, 100); // 初始化我方坦克
        // 初始化敌人坦克
        for (int i = 0; i < enemyTankSize; i++) {
            EnemyTank enemyTank = new EnemyTank(100 * (i + 1), 0);
            enemyTank.setDirect(2); // 设置方向向下
            enemyTanks.add(enemyTank);
        }
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillRect(0, 0, 1000, 750); // 背景
        // 绘制我方坦克(type=1)
        drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), 1);
        // 绘制敌人坦克(type=0)
        for (int i = 0; i < enemyTanks.size(); i++) {
            EnemyTank enemyTank = enemyTanks.get(i);
            drawTank(enemyTank.getX(), enemyTank.getY(), g, enemyTank.getDirect(), 0);
        }
    }

    /**
     * 绘制坦克(支持多方向)
     */
    public void drawTank(int x, int y, Graphics g, int direct, int type) {
        // 设置颜色
        switch (type) {
            case 0: // 敌人坦克(青色)
                g.setColor(Color.cyan);
                break;
            case 1: // 我方坦克(黄色)
                g.setColor(Color.yellow);
                break;
        }

        // 根据方向绘制
        switch (direct) {
            case 0: // 向上
                g.fill3DRect(x, y, 10, 60, false); // 左轮
                g.fill3DRect(x + 30, y, 10, 60, false); // 右轮
                g.fill3DRect(x + 10, y + 10, 20, 40, false); // 车身
                g.fillOval(x + 10, y + 20, 20, 20); // 盖子
                g.drawLine(x + 20, y + 30, x + 20, y); // 炮筒
                break;
            case 1: // 向右
                g.fill3DRect(x, y, 60, 10, false); // 上轮
                g.fill3DRect(x, y + 30, 60, 10, false); // 下轮
                g.fill3DRect(x + 10, y + 10, 40, 20, false); // 车身
                g.fillOval(x + 20, y + 10, 20, 20); // 盖子
                g.drawLine(x + 30, y + 20, x + 60, y + 20); // 炮筒
                break;
            case 2: // 向下
                g.fill3DRect(x, y, 10, 60, false); // 左轮
                g.fill3DRect(x + 30, y, 10, 60, false); // 右轮
                g.fill3DRect(x + 10, y + 10, 20, 40, false); // 车身
                g.fillOval(x + 10, y + 20, 20, 20); // 盖子
                g.drawLine(x + 20, y + 30, x + 20, y + 60); // 炮筒
                break;
            case 3: // 向左
                g.fill3DRect(x, y, 60, 10, false); // 上轮
                g.fill3DRect(x, y + 30, 60, 10, false); // 下轮
                g.fill3DRect(x + 10, y + 10, 40, 20, false); // 车身
                g.fillOval(x + 20, y + 10, 20, 20); // 盖子
                g.drawLine(x + 30, y + 20, x, y + 20); // 炮筒
                break;
            default:
                System.out.println("暂时没有处理");
        }
    }

    // 键盘监听方法
    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_W) { // 向上
            hero.setDirect(0);
            hero.moveUp();
        } else if (e.getKeyCode() == KeyEvent.VK_D) { // 向右
            hero.setDirect(1);
            hero.moveRight();
        } else if (e.getKeyCode() == KeyEvent.VK_S) { // 向下
            hero.setDirect(2);
            hero.moveDown();
        } else if (e.getKeyCode() == KeyEvent.VK_A) { // 向左
            hero.setDirect(3);
            hero.moveLeft();
        }
        this.repaint(); // 重绘
    }

    @Override
    public void keyTyped(KeyEvent e) {}
    @Override
    public void keyReleased(KeyEvent e) {}
}

/**
 * 坦克父类(增强移动方法)
 * @author 韩顺平
 * @version 1.0
 */
class Tank {
    private int x;
    private int y;
    private int direct = 0; // 方向:0-上,1-右,2-下,3-左
    private int speed = 1; // 移动速度

    public Tank(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // 移动方法
    public void moveUp() { y -= speed; }
    public void moveRight() { x += speed; }
    public void moveDown() { y += speed; }
    public void moveLeft() { x -= speed; }

    // getter 和 setter
    public int getX() { return x; }
    public void setX(int x) { this.x = x; }
    public int getY() { return y; }
    public void setY(int y) { this.y = y; }
    public int getDirect() { return direct; }
    public void setDirect(int direct) { this.direct = direct; }
    public int getSpeed() { return speed; }
    public void setSpeed(int speed) { this.speed = speed; }
}

本章作业

编程题(基于 HspTankGame02.java)

  1. 绘制三辆敌人坦克(颜色区分)
  2. 单独创建 EnemyTank 类(后续扩展特殊属性和方法)
  3. 使用 Vector 存储敌人坦克(考虑多线程安全)

本章内容小结

版本核心功能
HspTankGame01.java绘制我方坦克
HspTankGame02.java1. 绘制我方和敌人坦克
2. 按键(W/D/S/A)控制我方坦克移动

线程-应用到坦克大战

坦克大战 0.3 版(发射子弹)

核心思路
  1. 按下 J 键发射子弹,子弹为独立线程
  2. 子弹移动到面板边界时销毁
  3. 面板不停重绘子弹,实现射击效果

坦克大战 0.4 版(增强功能)

  1. 敌人坦克发射子弹(多颗子弹,用 Vector 存储)
  2. 我方坦克击中敌人坦克后,敌人坦克消失(可选爆炸效果)
  3. 敌人坦克随机移动(实现 Runnable 接口)
  4. 限制坦克移动范围
  5. 扩展:我方坦克最多发射5颗子弹,敌人坦克最多发射3颗子弹
敌人坦克发射子弹思路
  1. EnemyTank 类中用 Vector 存储 Shot 对象
  2. 创建 EnemyTank 时初始化 Shot 并启动线程
  3. 面板绘制时遍历 Vector,绘制所有子弹
  4. 子弹消亡(isLive = false)时从 Vector 移除
我方坦克发射限制思路
  1. Vector 存储我方子弹
  2. 按下 J 键时判断子弹数量是否超过上限
  3. 子弹消亡后从 Vector 移除

IO流-应用到坦克大战

坦克大战0.5版 新增功能

  1. 防止敌人坦克重叠运动
  2. 记录玩家击毁敌方坦克数,存盘退出
  3. 记录敌人坦克坐标/方向,存盘退出
  4. 支持选择开新游戏或继续上局游戏

核心思路

  • 数据持久化:使用IO流将击毁数、敌人坦克信息写入文件(myRecord.txt)
  • 数据恢复:游戏启动时读取文件,恢复上局进度
  • 防止重叠:通过坐标判断敌人坦克位置,避免重叠

坦克大战0.6版 新增功能

  1. 游戏开始时播放音乐
  2. 修正文件存储位置
  3. 处理文件相关异常,提高代码健壮性